/**
 * @license
 * Copyright (C) 2015 The Android Open Source Project
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 * http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
import '../../../styles/shared-styles.js';
import '../../shared/gr-dropdown-list/gr-dropdown-list.js';
import '../../shared/gr-select/gr-select.js';
import {dom} from '@polymer/polymer/lib/legacy/polymer.dom.js';
import {mixinBehaviors} from '@polymer/polymer/lib/legacy/class.js';
import {GestureEventListeners} from '@polymer/polymer/lib/mixins/gesture-event-listeners.js';
import {LegacyElementMixin} from '@polymer/polymer/lib/legacy/legacy-element-mixin.js';
import {PolymerElement} from '@polymer/polymer/polymer-element.js';
import {htmlTemplate} from './gr-patch-range-select_html.js';
import {PatchSetBehavior} from '../../../behaviors/gr-patch-set-behavior/gr-patch-set-behavior.js';
import {GrCountStringFormatter} from '../../shared/gr-count-string-formatter/gr-count-string-formatter.js';
import {appContext} from '../../../services/app-context.js';

// Maximum length for patch set descriptions.
const PATCH_DESC_MAX_LENGTH = 500;

/**
 * Fired when the patch range changes
 *
 * @event patch-range-change
 *
 * @property {string} patchNum
 * @property {string} basePatchNum
 * @extends PolymerElement
 */
class GrPatchRangeSelect extends mixinBehaviors( [
  PatchSetBehavior,
], GestureEventListeners(
    LegacyElementMixin(
        PolymerElement))) {
  static get template() { return htmlTemplate; }

  static get is() { return 'gr-patch-range-select'; }

  static get properties() {
    return {
      availablePatches: Array,
      _baseDropdownContent: {
        type: Object,
        computed: '_computeBaseDropdownContent(availablePatches, patchNum,' +
          '_sortedRevisions, changeComments, revisionInfo)',
      },
      _patchDropdownContent: {
        type: Object,
        computed: '_computePatchDropdownContent(availablePatches,' +
          'basePatchNum, _sortedRevisions, changeComments)',
      },
      changeNum: String,
      changeComments: Object,
      /** @type {{ meta_a: !Array, meta_b: !Array}} */
      filesWeblinks: Object,
      patchNum: String,
      basePatchNum: String,
      revisions: Object,
      revisionInfo: Object,
      _sortedRevisions: Array,
    };
  }

  static get observers() {
    return [
      '_updateSortedRevisions(revisions.*)',
    ];
  }

  constructor() {
    super();
    this.reporting = appContext.reportingService;
  }

  _getShaForPatch(patch) {
    return patch.sha.substring(0, 10);
  }

  _computeBaseDropdownContent(availablePatches, patchNum, _sortedRevisions,
      changeComments, revisionInfo) {
    // Polymer 2: check for undefined
    if ([
      availablePatches,
      patchNum,
      _sortedRevisions,
      changeComments,
      revisionInfo,
    ].includes(undefined)) {
      return undefined;
    }

    const parentCounts = revisionInfo.getParentCountMap();
    const currentParentCount = parentCounts.hasOwnProperty(patchNum) ?
      parentCounts[patchNum] : 1;
    const maxParents = revisionInfo.getMaxParents();
    const isMerge = currentParentCount > 1;

    const dropdownContent = [];
    for (const basePatch of availablePatches) {
      const basePatchNum = basePatch.num;
      const entry = this._createDropdownEntry(basePatchNum, 'Patchset ',
          _sortedRevisions, changeComments, this._getShaForPatch(basePatch));
      dropdownContent.push(Object.assign({}, entry, {
        disabled: this._computeLeftDisabled(
            basePatch.num, patchNum, _sortedRevisions),
      }));
    }

    dropdownContent.push({
      text: isMerge ? 'Auto Merge' : 'Base',
      value: 'PARENT',
    });

    for (let idx = 0; isMerge && idx < maxParents; idx++) {
      dropdownContent.push({
        disabled: idx >= currentParentCount,
        triggerText: `Parent ${idx + 1}`,
        text: `Parent ${idx + 1}`,
        mobileText: `Parent ${idx + 1}`,
        value: -(idx + 1),
      });
    }

    return dropdownContent;
  }

  _computeMobileText(patchNum, changeComments, revisions) {
    return `${patchNum}` +
        `${this._computePatchSetCommentsString(changeComments, patchNum)}` +
        `${this._computePatchSetDescription(revisions, patchNum, true)}`;
  }

  _computePatchDropdownContent(availablePatches, basePatchNum,
      _sortedRevisions, changeComments) {
    // Polymer 2: check for undefined
    if ([
      availablePatches,
      basePatchNum,
      _sortedRevisions,
      changeComments,
    ].includes(undefined)) {
      return undefined;
    }

    const dropdownContent = [];
    for (const patch of availablePatches) {
      const patchNum = patch.num;
      const entry = this._createDropdownEntry(
          patchNum, patchNum === 'edit' ? '' : 'Patchset ', _sortedRevisions,
          changeComments, this._getShaForPatch(patch));
      dropdownContent.push(Object.assign({}, entry, {
        disabled: this._computeRightDisabled(basePatchNum, patchNum,
            _sortedRevisions),
      }));
    }
    return dropdownContent;
  }

  _computeText(patchNum, prefix, changeComments, sha) {
    return `${prefix}${patchNum}` +
      `${this._computePatchSetCommentsString(changeComments, patchNum)}` +
        (` | ${sha}`);
  }

  _createDropdownEntry(patchNum, prefix, sortedRevisions, changeComments,
      sha) {
    const entry = {
      triggerText: `${prefix}${patchNum}`,
      text: this._computeText(patchNum, prefix, changeComments, sha),
      mobileText: this._computeMobileText(patchNum, changeComments,
          sortedRevisions),
      bottomText: `${this._computePatchSetDescription(
          sortedRevisions, patchNum)}`,
      value: patchNum,
    };
    const date = this._computePatchSetDate(sortedRevisions, patchNum);
    if (date) {
      entry['date'] = date;
    }
    return entry;
  }

  _updateSortedRevisions(revisionsRecord) {
    const revisions = revisionsRecord.base;
    this._sortedRevisions = this.sortRevisions(Object.values(revisions));
  }

  /**
   * The basePatchNum should always be <= patchNum -- because sortedRevisions
   * is sorted in reverse order (higher patchset nums first), invalid base
   * patch nums have an index greater than the index of patchNum.
   *
   * @param {number|string} basePatchNum The possible base patch num.
   * @param {number|string} patchNum The current selected patch num.
   * @param {!Array} sortedRevisions
   */
  _computeLeftDisabled(basePatchNum, patchNum, sortedRevisions) {
    return this.findSortedIndex(basePatchNum, sortedRevisions) <=
        this.findSortedIndex(patchNum, sortedRevisions);
  }

  /**
   * The basePatchNum should always be <= patchNum -- because sortedRevisions
   * is sorted in reverse order (higher patchset nums first), invalid patch
   * nums have an index greater than the index of basePatchNum.
   *
   * In addition, if the current basePatchNum is 'PARENT', all patchNums are
   * valid.
   *
   * If the current basePatchNum is a parent index, then only patches that have
   * at least that many parents are valid.
   *
   * @param {number|string} basePatchNum The current selected base patch num.
   * @param {number|string} patchNum The possible patch num.
   * @param {!Array} sortedRevisions
   * @return {boolean}
   */
  _computeRightDisabled(basePatchNum, patchNum, sortedRevisions) {
    if (this.patchNumEquals(basePatchNum, 'PARENT')) { return false; }

    if (this.isMergeParent(basePatchNum)) {
      // Note: parent indices use 1-offset.
      return this.revisionInfo.getParentCount(patchNum) <
          this.getParentIndex(basePatchNum);
    }

    return this.findSortedIndex(basePatchNum, sortedRevisions) <=
        this.findSortedIndex(patchNum, sortedRevisions);
  }

  _computePatchSetCommentsString(changeComments, patchNum) {
    if (!changeComments) { return; }

    const commentCount = changeComments.computeCommentCount({patchNum});
    const commentString = GrCountStringFormatter.computePluralString(
        commentCount, 'comment');

    const unresolvedCount = changeComments.computeUnresolvedNum({patchNum});
    const unresolvedString = GrCountStringFormatter.computeString(
        unresolvedCount, 'unresolved');

    if (!commentString.length && !unresolvedString.length) {
      return '';
    }

    return ` (${commentString}` +
        // Add a comma + space if both comments and unresolved
        (commentString && unresolvedString ? ', ' : '') +
        `${unresolvedString})`;
  }

  /**
   * @param {!Array} revisions
   * @param {number|string} patchNum
   * @param {boolean=} opt_addFrontSpace
   */
  _computePatchSetDescription(revisions, patchNum, opt_addFrontSpace) {
    const rev = this.getRevisionByPatchNum(revisions, patchNum);
    return (rev && rev.description) ?
      (opt_addFrontSpace ? ' ' : '') +
        rev.description.substring(0, PATCH_DESC_MAX_LENGTH) : '';
  }

  /**
   * @param {!Array} revisions
   * @param {number|string} patchNum
   */
  _computePatchSetDate(revisions, patchNum) {
    const rev = this.getRevisionByPatchNum(revisions, patchNum);
    return rev ? rev.created : undefined;
  }

  /**
   * Catches value-change events from the patchset dropdowns and determines
   * whether or not a patch change event should be fired.
   */
  _handlePatchChange(e) {
    const detail = {patchNum: this.patchNum, basePatchNum: this.basePatchNum};
    const target = dom(e).localTarget;
    const latestPatchNum = this.computeLatestPatchNum(this.availablePatches);
    if (target === this.$.patchNumDropdown) {
      if (detail.patchNum === e.detail.value) return;
      this.reporting.reportInteraction('right-patchset-changed',
          {
            previous: detail.patchNum,
            current: e.detail.value,
            latest: latestPatchNum,
          });
      detail.patchNum = e.detail.value;
    } else {
      if (this.patchNumEquals(detail.basePatchNum, e.detail.value)) return;
      this.reporting.reportInteraction('left-patchset-changed',
          {previous: detail.basePatchNum, current: e.detail.value});
      detail.basePatchNum = e.detail.value;
    }

    this.dispatchEvent(
        new CustomEvent('patch-range-change', {detail, bubbles: false}));
  }
}

customElements.define(GrPatchRangeSelect.is, GrPatchRangeSelect);
